package tools

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"gozero-microservices/gateway-api/internal/svc"
	"log"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"

	"github.com/olivere/elastic/v7"
)

/*
 +----------------------------------------------------------------------
 + Title        : olivere插件版elasticsearch
 + Author       : huwl
 + Version      : 1.0.0
 + Initial-Time : 2021-01-06
 + Last-time    : 这个文件最后修改的时间 + 修改人的名称
 + Desc         : olivere插件版elasticsearch
 +				: 一个应用只连接一个ElasticSearch集群，所以使用全局once即可。
 +				: 1.ES 客户端是重量级资源，包含连接池
 +				: 2.连接应该全局共享，而不是重复创建
 +				: 3.once.Do 提供了线程安全的单例模式
 +				: 4.这符合连接池设计的最佳实践
 +----------------------------------------------------------------------
*/

var (
	esInstance *PluginElasticSearch
	esOnce     sync.Once
	esErr      error
)

type PluginElasticSearch struct {
	elasticClient *elastic.Client
	settings      map[string]interface{}
}

type ElasticSearchConf struct {
	Host             []string `yaml:"host" json:"host"`
	Username         string   `yaml:"username" json:"username"`
	Password         string   `yaml:"password" json:"password"`
	NumberOfShards   int8     `yaml:"number_of_shards" json:"number_of_shards"`
	NumberOfReplicas int8     `yaml:"number_of_replicas" json:"number_of_replicas"`
	IsSsl            bool     `yaml:"is_ssl" json:"is_ssl"`
	CertPath         string   `yaml:"cert_path" json:"cert_path"`
}

/**
 * 创建ES实例[单例模式]
 *
 * @return *PluginElasticSearch, error
 * @author huwl
 */
func NewElasticSearchInstance(svcCtx *svc.ServiceContext) (*PluginElasticSearch, error) {
	esOnce.Do(func() {
		esc := &ElasticSearchConf{}
		// ++++++++++++ 使用本地配置方式开始 ++++++++++++
		switch svcCtx.Config.Env {
		case "dev":
			esc.Host = svcCtx.Config.EsConf.Dev.Host
			esc.Username = svcCtx.Config.EsConf.Dev.Username
			esc.Password = svcCtx.Config.EsConf.Dev.Password
			esc.NumberOfShards = svcCtx.Config.EsConf.Dev.NumberOfShards
			esc.NumberOfReplicas = svcCtx.Config.EsConf.Dev.NumberOfReplicas
			esc.IsSsl = svcCtx.Config.EsConf.Dev.IsSsl
			esc.CertPath = svcCtx.Config.EsConf.Dev.CertPath
		case "test":
			esc.Host = svcCtx.Config.EsConf.Test.Host
			esc.Username = svcCtx.Config.EsConf.Test.Username
			esc.Password = svcCtx.Config.EsConf.Test.Password
			esc.NumberOfShards = svcCtx.Config.EsConf.Test.NumberOfShards
			esc.NumberOfReplicas = svcCtx.Config.EsConf.Test.NumberOfReplicas
			esc.IsSsl = svcCtx.Config.EsConf.Test.IsSsl
			esc.CertPath = svcCtx.Config.EsConf.Dev.CertPath
		case "prod":
			esc.Host = svcCtx.Config.EsConf.Prod.Host
			esc.Username = svcCtx.Config.EsConf.Prod.Username
			esc.Password = svcCtx.Config.EsConf.Prod.Password
			esc.NumberOfShards = svcCtx.Config.EsConf.Prod.NumberOfShards
			esc.NumberOfReplicas = svcCtx.Config.EsConf.Prod.NumberOfReplicas
			esc.IsSsl = svcCtx.Config.EsConf.Prod.IsSsl
			esc.CertPath = svcCtx.Config.EsConf.Prod.CertPath
		}
		// ++++++++++++ 使用本地配置方式结束 ++++++++++++

		// ++++++++++++ 使用配置中心方式开始 ++++++++++++
		// InitConfigCenter(EsConfigCenter, esc)
		// ++++++++++++ 使用配置中心方式结束 ++++++++++++

		clientOptionFunc := elastic.SetHttpClient(&http.Client{})
		// 开启ssl,使用 https 认证方式
		if esc.IsSsl {
			rootCAs, _ := x509.SystemCertPool()
			// 读取证书文件
			certs, _ := os.ReadFile(esc.CertPath)
			rootCAs.AppendCertsFromPEM(certs)
			clientOptionFunc = elastic.SetHttpClient(&http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{
						// 如果没有使用证书则需要，跳过验证
						// InsecureSkipVerify: true,
						RootCAs: rootCAs,
					},
				},
			})
		}

		// 创建ES连接
		client, err := elastic.NewClient(
			clientOptionFunc,
			elastic.SetURL(esc.Host...),
			elastic.SetBasicAuth(esc.Username, esc.Password),
			elastic.SetSniff(false),
			elastic.SetHealthcheckInterval(10*time.Second),
			// elastic.SetRetrier(NewCustomRetrier()),
			elastic.SetGzip(true),
			elastic.SetErrorLog(log.New(os.Stderr, "ELASTIC ", log.LstdFlags)),
			elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags)),
			elastic.SetHeaders(http.Header{
				"X-Caller-Id": []string{"..."},
			}),
		)
		if err != nil {
			esErr = err
			return
		}

		esInstance = &PluginElasticSearch{
			elasticClient: client,
			settings: map[string]interface{}{
				"number_of_shards":   esc.NumberOfShards,
				"number_of_replicas": esc.NumberOfReplicas,
			},
		}
	})

	return esInstance, esErr
}

// //初始化es连接
// func init() {
// 	//读取elasticsearch配置
// 	esConfig, err := GetConfig("es", "yml")
// 	if err != nil {
// 		panic(err)
// 	}

// 	clientOptionFunc := elastic.SetHttpClient(&http.Client{})
// 	// 开启ssl,使用 https 认证方式
// 	if esConfig.GetBool(Env + ".is_ssl") {
// 		rootCAs, _ := x509.SystemCertPool()
// 		// 读取证书文件
// 		certs, _ := os.ReadFile(esConfig.GetString(Env + ".cert_path"))
// 		rootCAs.AppendCertsFromPEM(certs)
// 		clientOptionFunc = elastic.SetHttpClient(&http.Client{
// 			Transport: &http.Transport{
// 				TLSClientConfig: &tls.Config{
// 					// 如果没有使用证书则需要，跳过验证
// 					// InsecureSkipVerify: true,
// 					RootCAs: rootCAs,
// 				},
// 			},
// 		})
// 	}

// 	client, err := elastic.NewClient(
// 		clientOptionFunc,
// 		elastic.SetURL(esConfig.GetString(Env+".host")),
// 		elastic.SetBasicAuth(esConfig.GetString(Env+".username"), esConfig.GetString(Env+".password")),
// 		elastic.SetSniff(false),
// 		elastic.SetHealthcheckInterval(10*time.Second),
// 		// elastic.SetRetrier(NewCustomRetrier()),
// 		elastic.SetGzip(true),
// 		elastic.SetErrorLog(log.New(os.Stderr, "ELASTIC ", log.LstdFlags)),
// 		elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags)),
// 		elastic.SetHeaders(http.Header{
// 			"X-Caller-Id": []string{"..."},
// 		}),
// 	)

// 	if err != nil {
// 		panic(err)
// 	}
// 	defer client.Stop()

// 	elasticClient = client
// 	settings = map[string]interface{}{
// 		"number_of_shards":   esConfig.GetString(Env + ".number_of_shards"),
// 		"number_of_replicas": esConfig.GetString(Env + ".number_of_replicas"),
// 	}
// }

/**
 * 创建索引
 *
 * @param string index ------------------------------必传，索引
 * @param map[string]interface{} properties ---------必传，文档属性
 * params := map[string]interface{}{
 * 	"goods_id": map[string]interface{}{
 * 		"type":  "integer",
 * 		"store": true,
 * 		"index": true, 高版本不需要
 * 	},
 * 	"goods_name": map[string]interface{}{
 * 		"type":     "text",
 * 		"store":    true,
 * 		"index":    true,高版本不需要
 * 		"analyzer": "ik_smart",
 * 	},
 * }
 * @param map[string]interface{} settingsParams -----非必传，
 *	settingsParams = map[string]interface{}{
 * 		"number_of_shards":   esConfig.GetString(runMode + ".number_of_shards"),
 * 		"number_of_replicas": esConfig.GetString(runMode + ".number_of_replicas"),
 * }
 * @return error
 */
func (es *PluginElasticSearch) CreateIndex(index string, properties map[string]interface{}, settingsParams ...map[string]interface{}) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if len(properties) <= 0 {
		return errors.New("属性不能为空")
	}

	b, err := es.elasticClient.IndexExists(index).Do(context.Background())
	if err != nil {
		return err
	}
	if b {
		return errors.New("当前索引已存在")
	}

	settingsArgs := es.settings
	if len(settingsParams) > 0 {
		settingsArgs = settingsParams[0]
	}

	body := map[string]interface{}{
		"settings": settingsArgs,
		"mappings": map[string]interface{}{
			"properties": properties,
		},
	}

	jsonBody, err := json.Marshal(body)
	if err != nil {
		return err
	}

	result, err := es.elasticClient.CreateIndex(index).Body(string(jsonBody)).IncludeTypeName(false).Do(context.Background())
	if err != nil {
		return err
	}
	if !result.Acknowledged {
		return errors.New("创建索引失败")
	}

	return nil
}

/**
 * 删除索引
 *
 * @param string index ------------------------------必传，索引
 * @return error
 */
func (es *PluginElasticSearch) DeleteIndex(index string) error {
	if index == "" {
		return errors.New("索引不能为空")
	}

	result, err := es.elasticClient.DeleteIndex(index).Do(context.Background())
	if err != nil {
		return err
	}
	if !result.Acknowledged {
		return errors.New("创建索引失败")
	}

	return nil
}

/**
 * 获取mappings
 *
 * @param string index ------------------------------必传，索引
 * @return map[string]interface{}, error
 */
func (es *PluginElasticSearch) GetMappings(index string) (map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	result, err := es.elasticClient.GetMapping().Index(index).Do(context.Background())
	if err != nil {
		return nil, err
	}
	return result, nil
}

//修改mappings
func (es *PluginElasticSearch) EditMappings(index string) error {
	return nil
}

/**
 * 添加文档
 *
 * @param string index ------------------------------必传，索引
 * @param map[string]interface{} doc ----------------必传，文档数据
 * @param string routing ----------------------------必传,路由
 * @param interface arguments -----------------------非必传，其他参数
 * @return error
 */
func (es *PluginElasticSearch) AddDoc(index string, doc map[string]interface{}, routing string, arguments ...interface{}) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if len(doc) <= 0 {
		return errors.New("文档数据不能为空")
	}

	id := ""
	if len(arguments) > 0 {
		id = arguments[0].(string)
	}

	//数据转换成json格式
	jsonDoc, err := json.Marshal(doc)
	if err != nil {
		return err
	}

	elasticIndex := es.elasticClient.Index().Index(index)
	if id != "" {
		elasticIndex.Id(id)
	}

	// 设置路由
	if routing != "" {
		elasticIndex.Routing(routing)
	}

	_, err = elasticIndex.BodyString(string(jsonDoc)).Do(context.Background())
	if err != nil {
		return err
	}

	return nil
}

/**
 * 批量添加文档
 *
 * @param string index ------------------------------必传,索引
 * @param []map[string]interface{} docs -------------必传,文档数据
 *	docs = [
 *		map[
 *			contents:近日，湖南省第四届故事分享会在长沙举行青年”。
 *			create_time:2023-12-27 16:10:42
 *			news_id:1000
 *			publisher:精选热点资讯
 *			status:1
 *			title:湖南省肿瘤医院青年喜获
 *		]
 *		map[
 *			contents:近日，中办，国办面对2024年除施。
 *			create_time:2023-12-27 16:10:42
 *			news_id:1001
 *			publisher:云中赏雲
 *			status:1
 *			title:面对除夕不放假安排
 *		]
 *	]
 * @param string routing ----------------------------必传,路由
 * @param bool isAssignId ---------------------------必传,是否指定文档id
 * @param string idName -----------------------------必传,id名称
 * @return error
 */
func (es PluginElasticSearch) BatchAddDoc(index string, docs []map[string]interface{}, routing string, isAssignId bool, idName string) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if len(docs) <= 0 {
		return errors.New("文档数据不能为空")
	}

	bulkService := es.elasticClient.Bulk()
	for _, doc := range docs {
		bulkIndexRequest := elastic.NewBulkIndexRequest().Index(index)
		if isAssignId && idName != "" {
			id, ok := doc[idName]
			if ok {
				switch id.(type) {
				case int:
					bulkIndexRequest.Id(strconv.Itoa(id.(int)))
				default:
					bulkIndexRequest.Id(id.(string))
				}
			}
		}

		// 设置路由
		if routing != "" {
			bulkIndexRequest.Routing(routing)
		}

		req := bulkIndexRequest.Doc(doc)
		bulkService.Add(req)
	}

	_, err := bulkService.Do(context.Background())
	if err != nil {
		return err
	}

	return nil
}

/**
 * 根据ID获取单个文档
 *
 * @param string index ------------------------------必传，索引
 * @param string id ---------------------------------必传，ID
 * @param string routing ----------------------------必传,路由
 * @param interface arguments -----------------------非必传，其他参数
 * @return map[string]interface{}, error
 */
func (es PluginElasticSearch) GetDocById(index, id, routing string, arguments ...interface{}) (map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}
	if id == "" {
		return nil, errors.New("文档ID不能为空")
	}
	elasticService := es.elasticClient.Get()
	if routing != "" {
		elasticService.Routing(routing)
	}
	result, err := elasticService.Index(index).Id(id).Do(context.Background())
	if err != nil {
		return nil, err
	}

	//解析数据
	data := map[string]interface{}{}
	err = json.Unmarshal(result.Source, &data)
	if err != nil {
		return nil, err
	}

	return data, nil
}

/**
 * 根据ID修改文档
 *
 * @param string index ------------------------------必传，索引
 * @param string id ---------------------------------必传，ID
 * @param string routing ----------------------------必传,路由
 * @param map[string]interface{} params -------------必传，文档
 * @param interface arguments -----------------------非必传，其他参数
 * @return error
 */
func (es PluginElasticSearch) EditDocById(index, id, routing string, params map[string]interface{}, arguments ...interface{}) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if id == "" {
		return errors.New("文档ID不能为空")
	}
	if len(params) <= 0 {
		return errors.New("更新数据不能为空")
	}
	elasticService := es.elasticClient.Update()
	if routing != "" {
		elasticService.Routing(routing)
	}
	_, err := elasticService.Index(index).Id(id).Doc(params).Do(context.Background())
	if err != nil {
		return err
	}

	return nil
}

/**
 * 批量修改文档
 *
 * @param string index ------------------------------必传,索引
 * @param string routing ----------------------------必传,路由
 * @param map[string]interface{} params -------------必传,文档
 * @param interface arguments -----------------------非必传,其他参数
 * @return error
 */
func (es PluginElasticSearch) BatchEditDoc(index, routing string, params map[string]interface{}, arguments ...interface{}) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if len(params) <= 0 {
		return errors.New("更新数据不能为空")
	}

	bulkRequest := es.elasticClient.Bulk()

	// params map[int]interface{}, key为id, value为要更新的数据
	for id, v := range params {
		doc := elastic.NewBulkUpdateRequest().
			Index(index).
			Id(id).
			Doc(v).
			// true 无则插入, 有则更新, 设置为false时记录不存在将报错
			DocAsUpsert(false)
		bulkRequest.Add(doc)
	}
	// bulkResponse, err := bulkRequest.Do(context.Background())
	_, err := bulkRequest.Do(context.Background())
	if err != nil {
		// 获取操作失败的记录
		// bad := bulkResponse.Failed()
		// if len(bad) > 0 {
		// 	s, _ := jsoniter.MarshalToString(bad)
		// 	err = errors.New("部分记录更新失败 " + s)
		// }
		return err
	}

	return nil
}

/**
 * 根据主键删除文档
 *
 * @param string index ------------------------------必传，索引
 * @param string id ---------------------------------必传，ID
 * @param string routing ----------------------------必传,路由
 * @param interface arguments -----------------------非必传，其他参数
 * @return error
 */
func (es PluginElasticSearch) DeleteDocById(index, id, routing string, arguments ...interface{}) error {
	if index == "" {
		return errors.New("索引不能为空")
	}
	if id == "" {
		return errors.New("文档ID不能为空")
	}
	elasticService := es.elasticClient.Delete()
	if routing != "" {
		elasticService.Routing(routing)
	}
	_, err := elasticService.Index(index).Id(id).Do(context.Background())
	if err != nil {
		return err
	}

	return nil
}

/**
 * 自动补全搜索建议
 *
 * @param string index ------------------------------必传，索引
 * @param string name -------------------------------必传，参数
 * @param []string values ---------------------------必传，参数
 * @param interface arguments -----------------------非必传，其他参数
 * @return []map[string]interface{}, error
 */
func (es PluginElasticSearch) SuggestSearch(index string, name string, value string, field string, pageSize int, arguments ...interface{}) ([]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}
	if name == "" {
		return nil, errors.New("搜索名称不能为空")
	}
	if value == "" {
		return nil, errors.New("搜索值不能为空")
	}

	// GET /index_goods/_search
	// {
	// 	"suggest": {
	// 		"title_suggest": {
	// 			"text": " 曲面屏",
	// 			"completion": {
	// 				"field": "suggestion",
	// 				"skip_duplicates": true,
	// 				"size": 10
	// 			}
	// 		}
	// 	}
	// }
	// 创建补全建议
	completionSuggester := elastic.NewCompletionSuggester(name)
	// 搜索内容
	completionSuggester = completionSuggester.Text(value)
	// 搜索字段
	completionSuggester = completionSuggester.Field(field)
	// 去重复
	completionSuggester = completionSuggester.SkipDuplicates(true)
	// 条数
	completionSuggester = completionSuggester.Size(pageSize)
	result, err := es.elasticClient.Search().
		Index(index).
		Query(elastic.NewMatchAllQuery()).
		Suggester(completionSuggester).
		Pretty(true).Do(context.Background())
	if err != nil {
		return nil, err
	}

	suggestions := result.Suggest[name]
	options := suggestions[0].Options

	docs := []map[string]interface{}{}
	for _, option := range options {
		docs = append(docs, map[string]interface{}{
			"value": option.Text,
		})
	}

	return docs, nil
}

func (es PluginElasticSearch) BoolSearch(index string, params map[string]interface{}, page int, pageSize int, sortField string, sortMode bool, arguments ...interface{}) ([]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	offset := (page - 1) * pageSize

	boolQuery := elastic.NewBoolQuery()

	for key, values := range params {
		if key == "must" {
			items := values.(map[string]interface{})
			for k, item := range items {
				switch k {
				case "match":
					value := item.(map[string]interface{})
					for i, v := range value {
						boolQuery.Must(elastic.NewMatchQuery(i, v))
					}
				}
			}
		}
	}

	fmt.Println("boolQuery =========================== ", boolQuery)

	result, err := es.elasticClient.Search().
		Index(index).
		Query(boolQuery).
		Sort(sortField, sortMode).
		Pretty(true).
		From(offset).Size(pageSize).Do(context.Background())

	if err != nil {
		return nil, err
	}

	fmt.Println("result ===================================== ", result)

	return nil, nil
}

/**
 * 基础搜索+分页:from+size、search after
 *
 * @param string index --------必传，索引
 * @param elastic.Query query -必传，参数
 * @param string pageMode ------------必传,[size | after]
 * // 分页模式【from+size、search after】
 *	from+size
 * 	 - 优点:支持随机翻页
 *	 - 缺点:深度分页问题，默认查询上限（ from + size）是10000
 *	 - 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
 *	search after
 *	 - 优点:没有查询上限（单次查询的size不超过10000)
 *	 - 缺点:只能向后逐页查询，不支持随机翻页
 *	 - 场景:没有随机翻页需求的搜索，例如手机向下滚动翻页
 *
 * @param int page ------------必传，页码
 * @param int pageSize --------必传，单页条数
 * @param []interface{} lastSortValues ---------------必传,最后一条数据的排序值
 * @param string sortField ----必传，排序字段
 * @param bool sortMode -------必传，排序方式asc，desc
 * @param []string highlighterFields -----------------必传，高亮字段
 * @param interface{} arguments ----------------------必传，其他参数
 * @return map[string]interface{}, error
 */
func (es PluginElasticSearch) BaseSearch(index string, query elastic.Query, pageMode string, page, pageSize int, lastSortValues []interface{}, sortField string, sortMode string, highlighterFields []string, arguments ...interface{}) (map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	// 组装高亮字段
	hf := []*elastic.HighlighterField{}
	for _, highlighterField := range highlighterFields {
		hf = append(hf, elastic.NewHighlighterField(highlighterField))
	}

	highlight := elastic.NewHighlight()
	highlight.PreTags(`<em style="color:red">`)
	highlight.Fields(hf...)
	highlight.PostTags(`</em>`)
	offset := (page - 1) * pageSize
	ascending := true
	if sortMode == "desc" {
		ascending = false
	}

	// 分页模式: from+size、search after
	searchService := es.elasticClient.Search()
	searchService.Index(index).
		// 查询条件, 详细配置查询条件
		Query(query).
		// 显示实际文档数,默认显示10000
		TrackTotalHits(true).
		Highlight(highlight).
		Sort(sortField, ascending).
		Pretty(true)
		//指定返回数据的字段(此处指定返回id和name), 全部返回则无需设置
		// FetchSourceContext(elastic.NewFetchSourceContext(true).Include("id", "name")).

		// 分页模式
	switch pageMode {
	case "size":
		searchService.From(offset).Size(pageSize)
	case "after":
		searchService.SearchAfter(lastSortValues...).Size(pageSize)
	}

	result, err := searchService.Do(context.Background())
	if err != nil {
		return nil, err
	}

	docs := []map[string]interface{}{}
	// 定义排序值
	var sortValues []interface{}
	for _, v := range result.Hits.Hits {
		sortValues = v.Sort
		doc := map[string]interface{}{
			"doc_id": v.Id,
		}
		err := json.Unmarshal(v.Source, &doc)
		if err != nil {
			return nil, err
		}

		// 组装高亮字段值
		for _, value := range highlighterFields {
			fieldValue, ok := v.Highlight[value]
			if ok {
				doc[value] = fieldValue[0]
			}
		}

		docs = append(docs, doc)
	}

	// float64转字符串
	for i, v := range sortValues {
		switch v.(type) {
		case float64:
			sortValues[i] = strconv.FormatFloat(v.(float64), 'f', -1, 64)
		}
	}

	data := map[string]interface{}{
		"total":            result.TotalHits(),
		"last_sort_values": sortValues,
		"docs":             docs,
	}

	return data, nil
}

/**
 * scroll分页搜索
 *	 - 优点:没有查询上限（单次查询的size不超过10000)
 *	 - 缺点:会有额外内存消耗，并且搜索结果是非实时的
 *	 - 场景:海量数据的获取和迁移。从ES7.1开始不推荐，建议用after search方案。
 *	 - 由此也可以看出scroll不适合支持那种实时的和用户交互的前端分页工作，
 *   - 其主要用途用于从ES集群分批拉取大量结果集的情况，一般都是offline的应用场景。
 *	 - 比如需要将非常大的结果集拉取出来，存放到其他系统处理，或者需要做大索引的reindex等等。
 *	 - 不要把 scroll 用于实时请求，它主要用于大数据量的场景。
 *	 - 例如：将一个索引的内容索引到另一个不同配置的新索引中。
 *
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,参数
 * @param string scrollKeepAlive -----必传,每次查询都会使用这个时间续期
 * @param string scrollId ------------必传,scrollId
 * @param int pageSize ---------------必传,单页条数
 * @param string sortField -----------必传,排序字段
 * @param bool sortMode --------------必传,排序方式asc,desc
 * @param []string highlighterFields -必传,高亮字段
 * @param interface{} arguments ------必传,其他参数
 * @return map[string]interface{}, error
 */
func (es PluginElasticSearch) ScrollPageSearch(index string, query elastic.Query, scrollKeepAlive, scrollId string,
	pageSize int, sortField string, sortMode string, highlighterFields []string, arguments ...interface{}) (map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	// 组装高亮字段
	hf := []*elastic.HighlighterField{}
	for _, highlighterField := range highlighterFields {
		hf = append(hf, elastic.NewHighlighterField(highlighterField))
	}

	highlight := elastic.NewHighlight()
	highlight.PreTags(`<em style="color:red">`)
	highlight.Fields(hf...)
	highlight.PostTags(`</em>`)
	ascending := true
	if sortMode == "desc" {
		ascending = false
	}

	result, err := es.elasticClient.Scroll(index).Query(query).
		// 显示实际文档数,默认显示10000
		TrackTotalHits(true).
		Highlight(highlight).
		Sort(sortField, ascending).
		Pretty(true).
		// 这个时间只需要能查到第一次时间片就够了
		Scroll(scrollKeepAlive).
		ScrollId(scrollId).
		Size(pageSize).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	docs := []map[string]interface{}{}
	for _, v := range result.Hits.Hits {
		doc := map[string]interface{}{
			"doc_id": v.Id,
		}
		err := json.Unmarshal(v.Source, &doc)
		if err != nil {
			return nil, err
		}

		// 组装高亮字段值
		for _, value := range highlighterFields {
			fieldValue, ok := v.Highlight[value]
			if ok {
				doc[value] = fieldValue[0]
			}
		}

		docs = append(docs, doc)
	}

	data := map[string]interface{}{
		"total":     result.TotalHits(),
		"docs":      docs,
		"scroll_id": result.ScrollId,
	}

	return data, nil
}

/**
 * Terms 聚合
 *
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,搜索参数
 * @param map[string]interface{} aggregations ---------------必传,聚合
 * @return map[string][]map[string]interface{}, error
 */
func (es PluginElasticSearch) TermsAggregation(index string, query elastic.Query, aggregations map[string]interface{}) (map[string][]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

	for name, aggregation := range aggregations {
		searchService.Aggregation(name, aggregation.(elastic.Aggregation))
	}

	result, err := searchService.Do(context.Background()) // 执行请求

	if err != nil {
		return nil, errors.New(err.Error())
	}

	// 定义返回数据
	// 格式如：map[brand_aggs:[map[doc_count:1 key:BASTO/百思图] map[doc_count:1 key:Xiaomi/小米]] category_aggs:[map[doc_count:1 key: 空调柜机] map[doc_count:1 key:洁面]]]
	aggregationItems := map[string][]map[string]interface{}{}

	for name, _ := range aggregations {
		// 每项聚合结果
		aggregationValues := []map[string]interface{}{}
		aggregations, ok := result.Aggregations.Terms(name)
		if !ok {
			return nil, errors.New("没有找到聚合数据")
		}

		for _, v := range aggregations.Buckets {
			aggregationValues = append(aggregationValues, map[string]interface{}{
				"key":       v.Key,
				"doc_count": v.DocCount,
			})
		}

		// 将多项聚合结果组装在一起返回
		aggregationItems[name] = aggregationValues
	}

	return aggregationItems, nil
}

/**
 * TermsSub 聚合
 *
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,搜索参数
 * @param map[string]interface{} aggregations ---------------必传,聚合
 * @return map[string][]map[string]interface{}, error
 */
func (es PluginElasticSearch) TermsSubAggregation(index string, query elastic.Query, aggregations map[string]interface{}) (map[string][]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

	for name, aggregation := range aggregations {
		searchService.Aggregation(name, aggregation.(elastic.Aggregation))
	}

	result, err := searchService.Do(context.Background()) // 执行请求

	if err != nil {
		return nil, errors.New(err.Error())
	}

	// 定义返回数据
	aggregationItems := map[string][]map[string]interface{}{}

	for name, _ := range aggregations {
		// 每项聚合结果
		aggregationValues := []map[string]interface{}{}
		aggregations, ok := result.Aggregations.Terms(name)
		if !ok {
			return nil, errors.New("没有找到聚合数据")
		}

		// 遍历桶数据
		for _, value := range aggregations.Buckets {
			aggregationValue := map[string]interface{}{
				"key":       value.Key,
				"doc_count": value.DocCount,
			}
			for k, v := range value.Aggregations {
				if k != "key" && k != "doc_count" {
					var data interface{}
					json.Unmarshal(v, &data)
					switch data.(type) {
					case map[string]interface{}:
						aggregationValue[k] = data.(map[string]interface{})["value"]
					default:
						aggregationValue[k] = string(v)
					}
				}
			}
			aggregationValues = append(aggregationValues, aggregationValue)
		}

		// 将多项聚合结果组装在一起返回
		aggregationItems[name] = aggregationValues
	}

	return aggregationItems, nil
}

/**
 * Nested聚合
 *
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,搜索参数
 * @param map[string]interface{} aggregations ---------------必传,聚合
 * @return map[string][]map[string]interface{}, error
 */
func (es PluginElasticSearch) NestedAggregation(index string, query elastic.Query, aggregations map[string]interface{}) (map[string][]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

	for name, aggregation := range aggregations {
		searchService.Aggregation(name, aggregation.(elastic.Aggregation))
	}

	result, err := searchService.Do(context.Background()) // 执行请求

	if err != nil {
		return nil, errors.New(err.Error())
	}
	// fmt.Println("result =", result)

	// 定义返回数据
	// 格式如：map[brand_aggs:[map[doc_count:1 key:BASTO/百思图] map[doc_count:1 key:Xiaomi/小米]] category_aggs:[map[doc_count:1 key: 空调柜机] map[doc_count:1 key:洁面]]]
	aggregationItems := map[string][]map[string]interface{}{}

	// 解析组装数据
	for name, _ := range aggregations {
		// 每项聚合结果
		aggregationValues := []map[string]interface{}{}
		aggregations, ok := result.Aggregations.ReverseNested(name)
		if !ok {
			return nil, errors.New("没有找到聚合数据")
		}

		abki, _ := aggregations.Aggregations.RareTerms(name)

		for _, v := range abki.Buckets {
			aggregationValues = append(aggregationValues, map[string]interface{}{
				"key":       v.Key,
				"doc_count": v.DocCount,
			})
		}

		// 将多项聚合结果组装在一起返回
		aggregationItems[name] = aggregationValues
	}

	return aggregationItems, nil
}

/**
 * 综合聚合 = Nested聚合 + Terms桶聚合 + Range桶聚合 + Terms桶聚合
 *
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,搜索参数
 * @param map[string]map[string]interface{} aggregations ----必传,聚合
 * @return map[string][]map[string]interface{}, error
 */
func (es PluginElasticSearch) CompositeAggregation(index string, query elastic.Query, aggregations map[string]map[string]interface{}) (map[string][]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

	// 循环设置聚合项
	for _, aggregation := range aggregations {
		for name, aggr := range aggregation {
			searchService.Aggregation(name, aggr.(elastic.Aggregation))
		}
	}

	// 执行请求
	result, err := searchService.Do(context.Background())
	if err != nil {
		return nil, errors.New(err.Error())
	}

	// 定义聚合结果
	aggregationItems := map[string][]map[string]interface{}{}

	// 解析组装数据
	for aggregationType, aggregation := range aggregations {
		for name, _ := range aggregation {
			// 每项聚合结果
			aggregationValues := []map[string]interface{}{}
			var aggrs *elastic.AggregationBucketKeyItems
			ok := true
			switch aggregationType {
			case "terms":
				aggrs, ok = result.Aggregations.Terms(name)
			case "histogram", "date_histogram": // 直方图聚合
				var aggBucketHistogramItems *elastic.AggregationBucketHistogramItems
				var found bool
				if aggregationType == "histogram" {
					aggBucketHistogramItems, found = result.Aggregations.Histogram(name)
				} else if aggregationType == "date_histogram" {
					aggBucketHistogramItems, found = result.Aggregations.DateHistogram(name)
				}
				if !found {
					return nil, errors.New("没有找到聚合数据1")
				}

				for _, v := range aggBucketHistogramItems.Buckets {
					var key interface{}
					if aggregationType == "histogram" {
						key = v.Key
						// float64转字符串
						switch key.(type) {
						case float64:
							key = strconv.FormatFloat(key.(float64), 'f', -1, 64)
						}
					} else if aggregationType == "date_histogram" {
						key = *v.KeyAsString
					}
					aggregationValues = append(aggregationValues, map[string]interface{}{
						"key":       key,
						"doc_count": v.DocCount,
					})
				}
			case "range": // Range聚合
				aggBucketRangeItems, found := result.Aggregations.Range(name)
				if !found {
					return nil, errors.New("没有找到聚合数据")
				}

				for _, v := range aggBucketRangeItems.Buckets {
					aggregationValues = append(aggregationValues, map[string]interface{}{
						"key":       v.Key,
						"doc_count": v.DocCount,
					})
				}
			case "nested":
				aggregationSingleBucket, found := result.Aggregations.ReverseNested(name)
				if !found {
					return nil, errors.New("没有找到聚合数据")
				}

				aggrs, ok = aggregationSingleBucket.RareTerms(name)
			}

			if aggregationType == "terms" || aggregationType == "nested" {
				if !ok {
					return nil, errors.New("没有找到聚合数据")
				}
				for _, v := range aggrs.Buckets {
					aggregationValues = append(aggregationValues, map[string]interface{}{
						"key":       v.Key,
						"doc_count": v.DocCount,
					})
				}
			}

			// 将多项聚合结果组装在一起返回
			aggregationItems[name] = aggregationValues

		}
	}

	return aggregationItems, nil
}

/**
 * Metrics 聚合【Value Count、Cardinality、Avg、Sum、Max、Min、Stats】
 * Value Count：统计文档总数
 * Cardinality：去重统计文档总数
 * Avg：求平均值
 * Sum：求和计算
 * Max：求最大值
 * Min：求最小值
 * Stats：
 * @param string index ---------------必传,索引
 * @param elastic.Query query --------必传,搜索参数
 * @param map[string]map[string]interface{} aggregations ----必传,聚合
 * @return map[string][]map[string]interface{}, error
 */
func (es PluginElasticSearch) MetricsAggregation(index string, query elastic.Query, aggregations map[string]map[string]interface{}) ([]map[string]interface{}, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

	// 循环设置聚合项
	for _, aggregation := range aggregations {
		for name, aggr := range aggregation {
			searchService.Aggregation(name, aggr.(elastic.Aggregation))
		}
	}

	// 执行请求
	result, err := searchService.Do(context.Background())
	if err != nil {
		return nil, errors.New(err.Error())
	}

	// 定义聚合结果
	aggregationItems := []map[string]interface{}{}

	// 解析组装数据
	for aggregationType, aggregation := range aggregations {
		for name, _ := range aggregation {
			// 每项聚合结果
			aggregationValue := map[string]interface{}{}
			var aggrs *elastic.AggregationValueMetric
			var ok bool
			switch aggregationType {
			case "value_count":
				aggrs, ok = result.Aggregations.ValueCount(name)
			case "cardinality":
				aggrs, ok = result.Aggregations.Cardinality(name)
			case "avg":
				aggrs, ok = result.Aggregations.Avg(name)
			case "sum":
				aggrs, ok = result.Aggregations.Sum(name)
			case "max":
				aggrs, ok = result.Aggregations.Max(name)
			case "min":
				aggrs, ok = result.Aggregations.Min(name)
			case "stats":
				aggregationStatsMetric, b := result.Aggregations.Stats(name)
				if !b {
					return nil, errors.New("没有找到聚合数据")
				}

				// 解析数据
				statsMetricValues := map[string]interface{}{
					"count": aggregationStatsMetric.Count,
					"min":   *aggregationStatsMetric.Avg,
					"max":   *aggregationStatsMetric.Max,
					"avg":   *aggregationStatsMetric.Avg,
					"sum":   *aggregationStatsMetric.Sum,
				}

				// float64转字符串
				for k, v := range statsMetricValues {
					switch v.(type) {
					case float64:
						statsMetricValues[k] = strconv.FormatFloat(v.(float64), 'f', -1, 64)
					}
				}

				// 组装数据
				aggregationValue = map[string]interface{}{
					name: statsMetricValues,
				}
			}

			// 非 stats 聚合
			if aggregationType != "stats" {
				if !ok {
					return nil, errors.New("没有找到聚合数据")
				}

				aggregationValue = map[string]interface{}{
					name: *aggrs.Value,
				}

				// float64转字符串
				for k, v := range aggregationValue {
					switch v.(type) {
					case float64:
						aggregationValue[k] = strconv.FormatFloat(v.(float64), 'f', -1, 64)
					}
				}
			}

			aggregationItems = append(aggregationItems, aggregationValue)
		}
	}

	return aggregationItems, nil
}

/**
 * 调用方自己解析返回数据的聚合支持任何类型的聚合方式
 *
 * @param string index ---------------必传,索引
 * @param string aggName -------------必传,聚合名称
 * @param elastic.Query query --------必传,搜索参数
 * @param elastic.Aggregation aggregations 必传,聚合
 * @return *elastic.SearchResult, error
 */
func (es PluginElasticSearch) CustomParseResultAggregation(index string, query elastic.Query, aggName string, aggregations elastic.Aggregation) (*elastic.SearchResult, error) {
	if index == "" {
		return nil, errors.New("索引不能为空")
	}

	searchService := es.elasticClient.Search().
		Index(index).
		Query(query).
		// 设置分页参数 - 每页大小,设置为0代表不返回搜索结果，仅返回聚合分析结果
		Size(0)

		// 设置聚合项
	searchService.Aggregation(aggName, aggregations)

	// 执行请求
	return searchService.Do(context.Background())
}

/**
 * 关闭
 * @author huwl
 * @date 2025/11/04 13:41:01
 */
func CloseElasticSearch() {
	if esInstance != nil && esInstance.elasticClient != nil {
		esInstance.elasticClient.Stop()
		esInstance = nil
	}
}
